﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using NorthwindModel.IRepositories;
using System.Data;
using System.Data.Objects;
using NorthwindModel;
using System.Data.Objects.DataClasses;
using NorthwindModel.Models;

namespace NorthwindDAL.Repositories
{
    public class BaseRepository<T> : IDisposable, IBaseRepository<T> where T : BaseModel
    {
        private string entitySetName;
        private ObjectSet<T> ObjectSet;
        protected virtual NorthwindContext DataContext
        {
            get;
            set;
        }

        public BaseRepository(IUnitOfWork unitOfWork)
        {
            if (unitOfWork == null)
                throw new ArgumentNullException("unitOfWork");

            DataContext = unitOfWork as NorthwindContext;
            entitySetName = GetEntitySetName(DataContext, typeof(T));
            ObjectSet = DataContext.CreateObjectSet<T>();
        }

        public virtual int Commit()
        {
            return DataContext.SaveChanges();
        }

        public virtual void Update(T entity)
        {
            EntityKey entityKey = DataContext.CreateEntityKey(entitySetName, entity);
            object objectFound;
            if (DataContext.TryGetObjectByKey(entityKey, out objectFound))
            {
                T entityFound = (T)objectFound;
                ObjectSet.ApplyCurrentValues(entity);
            }
            else
            {
                ObjectSet.Attach(entity);
                DataContext.ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);
            }
        }

        public virtual void Add(T entity)
        {
            EntityKey entityKey = DataContext.CreateEntityKey(entitySetName, entity);
            object objectFound;
            if (!DataContext.TryGetObjectByKey(entityKey, out objectFound))
            {
                ObjectSet.AddObject(entity);
            }
        }

        public virtual void Delete(T entity)
        {
            EntityKey entityKey = DataContext.CreateEntityKey(entitySetName, entity);
            object objectFound;
            if (DataContext.TryGetObjectByKey(entityKey, out objectFound))
            {
                T entityFound = (T)objectFound;
                ObjectSet.DeleteObject(entityFound);
            }
            else
            {
                ObjectSet.Attach(entity);
                DataContext.ObjectStateManager.ChangeObjectState(entity, EntityState.Deleted);
            }
        }

        public virtual void Save(T entity)
        {
            if (entity.EntityState == EntityState.Added)
            {
                ObjectSet.AddObject(entity);
            }
            else
            {
                EntityKey customerKey = DataContext.CreateEntityKey(entitySetName, entity);
                object objectFound;
                if (DataContext.TryGetObjectByKey(customerKey, out objectFound))
                {
                    T entityFound = (T)objectFound;
                    if (entity.EntityState == EntityState.Modified)
                    {
                        ObjectSet.ApplyCurrentValues(entity);
                    }
                    else if (entity.EntityState == EntityState.Deleted)
                    {
                        ObjectSet.DeleteObject(entityFound);
                    }
                }
                else //entity can not be found in the DataContext
                {
                    //attach it to DataContect and change its state in the ObjectStateManage to current state of it
                    ObjectSet.Attach(entity);
                    DataContext.ObjectStateManager.ChangeObjectState(entity, entity.EntityState);
                }
            }
        }

        public virtual IQueryable<T> GetAll()
        {
            return ObjectSet;
        }

        public virtual T FindSingleBy(Expression<Func<T, bool>> predicate)
        {
            if (predicate != null)
            {
                return ObjectSet.Where(predicate).SingleOrDefault();
            }
            throw new ArgumentNullException("Predicate value must be passed to FindSingleBy<T>.");
        }

        public virtual IQueryable<T> FindAllBy(Expression<Func<T, bool>> predicate)
        {
            if (predicate != null)
            {
               return ObjectSet.Where(predicate).AsQueryable<T>(); 
            }
            throw new ArgumentNullException("Predicate value must be passed to FindBy<T,TKey>.");

        }

        public IQueryable<T> GetIncluding(params Expression<Func<T, object>>[] includeProperties)
        {
            IQueryable<T> query = ObjectSet;
            foreach (var o in query)
            {
                foreach (var includeProperty in includeProperties)
                {
                    //query = query.Include(includeProperty);
                    DataContext.LoadProperty(o, includeProperty);
                }
            }
            return query;
        }
        
        public void Dispose()
        {
            if (DataContext != null) DataContext.Dispose();
        }

        /// <summary>
        /// Returns entity set name for a given entity type
        /// </summary>
        /// <param name="context">An ObjectContext which defines the entity set for entityType. Must be non-null.</param>
        /// <param name="entityType">An entity type. Must be non-null and have an entity set defined in the context argument.</param>
        /// <exception cref="ArgumentException">If entityType is not an entity or has no entity set defined in context.</exception>
        /// <returns>String name of the entity set.</returns>
        private string GetEntitySetName(ObjectContext context, Type entityType)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }
            if (entityType == null)
            {
                throw new ArgumentNullException("entityType");
            }
            // when POCO proxies are enabled, "entityType" may be a subtype of the mapped type.
            Type nonProxyEntityType = ObjectContext.GetObjectType(entityType);
            if (entityType == null)
            {
                throw new ArgumentException(entityType.Name+" is not an Entity");
            }

            var container = context.MetadataWorkspace.GetEntityContainer(context.DefaultContainerName, System.Data.Metadata.Edm.DataSpace.CSpace);
            var result = (from entitySet in container.BaseEntitySets
                          where entitySet.ElementType.Name.Equals(nonProxyEntityType.Name)
                          select entitySet.Name).SingleOrDefault();
            if (string.IsNullOrEmpty(result))
            {
                throw new ArgumentException(entityType.Name + " is not an Entity");
            }
            return result;
        }
    }
}
